xtask\tasks\fmt\house_rules/
trailing_newline.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use anyhow::anyhow;
5use fs_err::File;
6use fs_err::OpenOptions;
7use std::io::Read;
8use std::io::Seek;
9use std::io::SeekFrom;
10use std::io::Write;
11use std::path::Path;
12
13pub fn check_trailing_newline(path: &Path, fix: bool) -> anyhow::Result<()> {
14    let ext = path
15        .extension()
16        .and_then(|e| e.to_str())
17        .unwrap_or_default();
18
19    if !matches!(
20        ext,
21        "c" | "md" | "proto" | "py" | "rs" | "sh" | "toml" | "txt" | "yml" | "js" | "ts"
22    ) {
23        return Ok(());
24    }
25
26    // workaround for `mdbook-docfx` emitting yaml with no trailing newline
27    if path.file_name().unwrap() == "toc.yml" {
28        return Ok(());
29    }
30
31    let mut f = OpenOptions::new().read(true).write(fix).open(path)?;
32    f.seek(SeekFrom::End(-2))?;
33    let mut b = [0; 2];
34    f.read_exact(&mut b)?;
35
36    let missing_single_trailing_newline = !(b[0] != b'\n' && b[1] == b'\n');
37
38    if missing_single_trailing_newline {
39        if fix {
40            let truncate_to = find_first_trailing_nl(&mut f)?;
41            f.set_len(truncate_to)?;
42            f.seek(SeekFrom::End(0))?;
43            writeln!(f)?;
44        } else {
45            // just report the error
46            return Err(anyhow!(
47                "missing single trailing newline in {}",
48                path.display()
49            ));
50        }
51    }
52
53    Ok(())
54}
55
56// implementing this function efficiently requires reading the file backwards,
57// which is kinda annoying...
58fn find_first_trailing_nl(f: &mut File) -> std::io::Result<u64> {
59    const BLOCK_SIZE: u64 = 512;
60
61    let mut pos = f.seek(SeekFrom::End(0))?;
62    let mut file_block = [0; BLOCK_SIZE as usize];
63    while pos != 0 {
64        let new_pos = pos.saturating_sub(BLOCK_SIZE);
65        let delta = pos - new_pos;
66        pos = new_pos;
67
68        let file_block = &mut file_block[..delta as usize];
69        f.seek(SeekFrom::Start(pos))?;
70        f.read_exact(file_block)?;
71
72        let num_trailing_newlines =
73            file_block.iter().rev().take_while(|x| **x == b'\n').count() as u64;
74
75        match num_trailing_newlines {
76            0 => {
77                // no trailing newlines in this block at all
78                pos += delta;
79                break;
80            }
81            n if n == delta => {
82                // it's all newlines, so we keep on going
83            }
84            n => {
85                // nice, we found the start of the newlines
86                pos += delta - n;
87                break;
88            }
89        }
90    }
91
92    Ok(pos)
93}